﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.ComponentModel;
using System.Windows;

namespace WpfDecorator
{
    /// <summary>
    /// Represents a Decorator element that can provide expandable/collapsible 
    /// effects to a set of UI elements.
    /// </summary>
    public class XPandableDecorator : Decorator
    {
        #region [       Fields       ]

        //Storyboard used to provide expandable/collapsible effects
        private Storyboard _stBoard = null;

        #endregion


        #region [       Constructor       ]

        //===========================================================================
        /// <summary>
        /// Static constructor used to override the dependency properties (if
        /// needed) and define the default style for the control.
        /// </summary>
        //===========================================================================
        static XPandableDecorator()
        {
            //Sets the default value of the 'ClipToBounds' dependency property to 'true'
            ClipToBoundsProperty.OverrideMetadata(
                typeof(XPandableDecorator), new FrameworkPropertyMetadata(true));

            //Sets the default value of the 'Opacity' dependency property to '0.0'
            OpacityProperty.OverrideMetadata(
                typeof(XPandableDecorator), new FrameworkPropertyMetadata(0.0));

            //Sets the default value of the 'Focusable' dependency property to 'false'
            FocusableProperty.OverrideMetadata(
                typeof(XPandableDecorator), new FrameworkPropertyMetadata(false));
        }

        #endregion


        #region [       Dependency properties       ]

        #region IsExpanded property

        //===========================================================================
        /// <summary>
        /// Gets or sets a value indicating whether the decorator is expanded.
        /// This is a dependency property.
        /// </summary>
        /// <remarks>
        /// The default value is <c>false</c>.
        /// </remarks>
        //===========================================================================
        public bool IsExpanded
        {
            get { return (bool)GetValue(XPandableDecorator.IsExpandedProperty); }
            set { SetValue(XPandableDecorator.IsExpandedProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="IsExpanded"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty IsExpandedProperty =
            DependencyProperty.Register("IsExpanded",
            typeof(bool), typeof(XPandableDecorator),
            new PropertyMetadata(false, new PropertyChangedCallback(OnIsExpandedChanged)));

        /// <summary>
        /// Invoked whenever the <c>IsExpanded</c> dependency property value
        /// has been updated.
        /// </summary>
        /// <param name="sender">The <c>DependencyObject</c> on which the property
        /// has changed value.</param>
        /// <param name="e">Event data that is issued by any event that tracks changes 
        /// to the effective value of this property. </param>
        private static void OnIsExpandedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            //Gets the XPandableDecorator instance who sent the event
            XPandableDecorator expDecorator = (XPandableDecorator)sender;

            //Gets the new value of the 'IsExpanded' DP
            bool IsExpandedNewValue = (bool)e.NewValue;

            //Creates a new storyboard or stops it if it already exists
            if (expDecorator._stBoard == null)
                expDecorator._stBoard = new Storyboard();
            else
                expDecorator._stBoard.Stop(expDecorator);

            //Creates a new animation for a double value
            DoubleAnimation animation = new DoubleAnimation();
            animation.To = (IsExpandedNewValue) ? 1.0 : 0.0;
            animation.Duration =
                (expDecorator.IsLoaded) ?
                TimeSpan.FromMilliseconds(expDecorator.ExpandOrCollapseDuration) :
                TimeSpan.FromMilliseconds(0.0);
            animation.AccelerationRatio = (IsExpandedNewValue) ? 0.0 : 0.33;
            animation.DecelerationRatio = (IsExpandedNewValue) ? 0.33 : 0.0;
            animation.FillBehavior = FillBehavior.HoldEnd;

            //Links it to the 'AnimationProgress' dependency property
            Storyboard.SetTargetProperty(animation, new PropertyPath(AnimationProgressProperty));

            //Clears previous animations and adds the new one to the storyboard
            expDecorator._stBoard.Children.Clear();
            expDecorator._stBoard.Children.Add(animation);

            //Starts the storyboard and sets it as controllable
            expDecorator._stBoard.Begin(expDecorator, true);
        }

        #endregion

        #region AnimationProgress property

        //===========================================================================
        /// <summary>
        /// Gets or sets a value indicating the progression of the animation (the 
        /// value goes from 0 to 1). This is a dependency property.
        /// </summary>
        /// <remarks>
        /// The default value is 0 (decorator collapsed).
        /// </remarks>
        //===========================================================================
        [Browsable(false)]
        public double AnimationProgress
        {
            get { return (double)GetValue(XPandableDecorator.AnimationProgressProperty); }
            set { SetValue(XPandableDecorator.AnimationProgressProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="AnimationProgress"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty AnimationProgressProperty =
            DependencyProperty.Register("AnimationProgress",
            typeof(double), typeof(XPandableDecorator),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure,
                new PropertyChangedCallback(OnAnimationProgressChanged),
                new CoerceValueCallback(CoerceAnimationProgress)));

        /// <summary>
        /// Invoked whenever the <c>AnimationProgress</c> dependency property value
        /// has been updated.
        /// </summary>
        /// <param name="sender">The <c>DependencyObject</c> on which the property
        /// has changed value.</param>
        /// <param name="e">Event data that is issued by any event that tracks changes 
        /// to the effective value of this property. </param>
        private static void OnAnimationProgressChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            //Gets the XPandableDecorator instance who sent the event
            XPandableDecorator expDecorator = (XPandableDecorator)sender;

            //Updates the instance Visibility
            expDecorator.Visibility =
                (expDecorator.AnimationProgress > 0.0) ? Visibility.Visible : Visibility.Hidden;

            //Updates the instance Opacity
            expDecorator.Opacity = expDecorator.AnimationProgress;
        }

        /// <summary>
        /// Invoked whenever the <c>AnimationProgress</c> dependency property value is 
        /// being re-evaluated, or coercion is specifically requested.
        /// </summary>
        /// <param name="sender">Not used here.</param>
        /// <param name="value">The new value of the property, prior to any coercion 
        /// attempt.</param>
        /// <returns>The coerced value.</returns>
        private static object CoerceAnimationProgress(DependencyObject sender, object value)
        {
            //Gets the value to coerce
            double animationProgress = (double)value;

            //Keeps the value between 0 and 1
            if (animationProgress < 0.0)
                animationProgress = 0.0;
            else if (animationProgress > 1.0)
                animationProgress = 1.0;

            return animationProgress;
        }

        #endregion

        #region ExpandCollapseDuration property

        //===========================================================================
        /// <summary>
        /// Gets or sets the duration (in milliseconds) of the expanding 
        /// (or collapsing) animation. This is a dependency property.
        /// </summary>
        /// <remarks>
        /// The default value is 1000 milliseconds.
        /// </remarks>
        //===========================================================================
        public Double ExpandOrCollapseDuration
        {
            get { return (Double)GetValue(XPandableDecorator.ExpandOrCollapseDurationProperty); }
            set { SetValue(XPandableDecorator.ExpandOrCollapseDurationProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="ExpandOrCollapseDuration"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ExpandOrCollapseDurationProperty =
            DependencyProperty.Register("ExpandOrCollapseDuration",
            typeof(Double), typeof(XPandableDecorator),
            new FrameworkPropertyMetadata(1000.0));

        #endregion

        #endregion


        #region [       Layout process       ]

        //===========================================================================
        /// <summary>
        /// Measures the size in layout required for its child element and determines 
        /// the size for this element.
        /// </summary>
        /// <param name="availableSize">The available size that this element can give 
        /// to its child element. Infinity can be specified as a value to indicate 
        /// that the element will size to whatever content is available.</param>
        /// <returns>The size that this element determines it needs during layout, 
        /// based on its calculations of child element sizes.</returns>
        //===========================================================================
        protected override Size MeasureOverride(Size availableSize)
        {
            //Gets the single child of the Border
            UIElement singleChild = Child;

            //Checks if it exists
            if (singleChild != null)
            {
                //Asks the child how big it wants to be within the available area
                singleChild.Measure(availableSize);

                //Evaluates the height of the element according to child item desired 
                //height and the animation progress
                double height = singleChild.DesiredSize.Height * AnimationProgress;

                //Returns the original width associated with the calculated height
                return new Size(availableSize.Width, height);
            }
            return new Size();
        }


        //===========================================================================
        /// <summary>
        /// Positions child elements and determines a size for this element.
        /// </summary>
        /// <param name="arrangeSize">The final area within the parent this 
        /// element should use to arrange itself and its children.</param>
        /// <returns>The actual size used.</returns>
        //===========================================================================
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            //Gets the single child of the Border
            UIElement singleChild = Child;

            //Checks if it exists
            if (singleChild != null)
            {
                //Calculates the y-coordinate of the top left corner of the child
                double y = singleChild.DesiredSize.Height * (AnimationProgress - 1.0);

                //Tells the single child its location and size
                singleChild.Arrange(new Rect(new Point(0.0, y),
                    new Size(arrangeSize.Width, singleChild.DesiredSize.Height)));

                //Returns the original size
                return arrangeSize;
            }
            return new Size();
        }

        #endregion
    }
}
